package com.mercycrazy.recruit.filter;

import com.alibaba.fastjson.JSON;
import com.mercycrazy.recruit.config.IpLimitConfig;
import com.mercycrazy.recruit.enums.StatusCode;
import com.mercycrazy.recruit.util.IPUtil;
import com.mercycrazy.recruit.vo.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.mercycrazy.recruit.constant.RedisConstant.GATEWAY_BLACK_IP;
import static com.mercycrazy.recruit.constant.RedisConstant.GATEWAY_LIMIT_IP_REQUEST_COUNTS;

/**
 * IP 黑名单网关限流过滤器
 *
 * @author <a href="mailto:1443424326@qq.com">mercycrazy</a>
 * @since 2023-01-16
 */
@Slf4j
@Component
@SuppressWarnings("all")
public class IPLimitFilter implements GlobalFilter, Ordered {

    private final IpLimitConfig ipLimitConfig;

    private final StringRedisTemplate redisTemplate;

    // 路径匹配规则器
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    public IPLimitFilter(IpLimitConfig ipLimitConfig,
                         StringRedisTemplate redisTemplate) {
        this.ipLimitConfig = ipLimitConfig;
        this.redisTemplate = redisTemplate;
    }

    @Value("${blackIP.continueCounts}")
    private Integer continueCounts;
    @Value("${blackIP.timeInterval}")
    private Integer timeInterval;
    @Value("${blackIP.limitTimes}")
    private Integer limitTimes;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // 1. 获取当前的请求路径
        String url = exchange.getRequest().getURI().getPath();

        // 2. 获得所有的需要进行 ip 限流校验的 url
        List<String> ipLimitUrls = ipLimitConfig.getIpLimitUrls();

        // 3. 校验并且判断
        if (CollectionUtils.isNotEmpty(ipLimitUrls)) {
            for (String limitUrl : ipLimitUrls) {
                if (antPathMatcher.matchStart(limitUrl, url)) {
                    // 如果匹配到, 则表明需要进行 ip 的拦截校验
                    return doLimit(exchange, chain);
                }
            }
        }

        // 4. 默认直接放行
        return chain.filter(exchange);
    }

    /**
     * 校验并且执行 IP 的限流拦截
     */
    private Mono<Void> doLimit(ServerWebExchange exchange,
                               GatewayFilterChain chain) {

        // 获取 request ip
        String requestIp = IPUtil.getRequestIpFromGateway(exchange.getRequest());

        // 关小黑屋的 IP 的 Redis Key
        final String ipRedisBlackKey = String.format(GATEWAY_BLACK_IP, requestIp);
        // 统计 IP 连续请求次数的 Redis Key
        final String ipRedisLimitKey = String.format(
                GATEWAY_LIMIT_IP_REQUEST_COUNTS, requestIp);

        // 查询一下当前 IP 是否已经被关小黑屋
        if (redisTemplate.hasKey(ipRedisBlackKey)) {
            return renderErrorMsg(exchange, StatusCode.SYSTEM_ERROR_BLACK_IP);
        }

        // 在 redis 中累加 ip 的请求访问次数
        long requestCounts = redisTemplate.opsForValue().increment(
                ipRedisLimitKey, 1
        );

        // requestCounts == 1 代表第一次访问, 需要设置过期时间, 即允许连续请求的时间
        if (requestCounts == 1) {
            redisTemplate.expire(ipRedisLimitKey, timeInterval, TimeUnit.SECONDS);
        }

        // 如果获取一个大于 1 的请求次数, 说明用户连续请求的次数落在 10s 内
        // 请求次数超过允许连续访问的次数, 需要进行限流
        if (requestCounts > continueCounts) {
            // IP 关进小黑屋
            redisTemplate.opsForValue().set(
                    ipRedisBlackKey, requestIp, limitTimes, TimeUnit.SECONDS
            );

            // 终止请求, 返回错误
            return renderErrorMsg(exchange, StatusCode.SYSTEM_ERROR_BLACK_IP);
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * Gateway 基于 Webflux, 不走 Spring MVC 全局异常处理器, 所以需要对异常处理单独包装
     */
    private Mono<Void> renderErrorMsg(ServerWebExchange exchange,
                                      StatusCode statusCode) {

        ServerHttpResponse response = exchange.getResponse();

        response.setStatusCode(HttpStatus.UNAUTHORIZED);

        ApiResponse<?> errorResponse = new ApiResponse<>(statusCode);

        if (!response.getHeaders().containsKey("Content-Type")) {
            response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        }

        String resultJson = JSON.toJSONString(errorResponse);

        DataBuffer dataBuffer = response.bufferFactory()
                .wrap(resultJson.getBytes(StandardCharsets.UTF_8));

        return response.writeWith(Mono.just(dataBuffer));
    }
}
